#include "defines.h"
#include "elf.h"
#include "lib.h"
#include "xmodem.h"

struct elf_header {
    struct {
        unsigned char magic[4];
        unsigned char class;
        unsigned char format;
        unsigned char version;
        unsigned char abi;
        unsigned char abi_version;
        unsigned char reserve[7];
    } id;
    short type;
    short arch;
    long version;
    long entry_point;
    long program_header_offset;
    long section_header_offset;
    long flags;
    short header_size;
    short program_header_size;
    short program_header_num;
    short section_header_size;
    short section_header_num;
    short section_name_index;
};

struct elf_program_header {
    long type;
    long offset;
    long virtual_addr;
    long physical_addr;
    long file_size;
    long memory_size;
    long flags;
    long align;
};

static int elf_check(struct elf_header *header)
{
    if (memcmp(header->id.magic, "\x7f" "ELF", 4)) return -1;

    if (header->id.class != 1) return -1;                           /* 32bit */
    if (header->id.format != 2) return -1;                          /* Big endian */
    if (header->id.version != 1) return -1;                         /* Current version */
    if (header->type != 2) return -1;                               /* Executable file */
    if (header->version != 1) return -1;                            /* Current version */
    if ((header->arch != 46) && (header->arch != 47)) return -1;    /* H8/300(H) */

    return 0;
}

static int elf_load_program(struct elf_header *header)
{
    int i;
    struct elf_program_header *phdr;

    for (i = 0; i < header->program_header_num; i++) {
        phdr = (struct elf_program_header *) ((char *)header + header->program_header_offset + header->program_header_size * i);
        if (phdr->type != 1)
            continue;

        memcpy((char *)phdr->physical_addr, (char *)header + phdr->offset, phdr->file_size);
        memset((char *)phdr->physical_addr + phdr->file_size, 0, phdr->memory_size - phdr->file_size);
    }

    return 0;
}

char *elf_load(char *buf)
{
    struct elf_header *header = (struct elf_header *)buf;

    if (!buf)
        return NULL;

    if (elf_check(header) < 0)
        return NULL;

    if (elf_load_program(header) < 0)
        return NULL;

    return (char *)header->entry_point;
}

static void load_block(char *block, long bend_off, struct elf_program_header *phdr)
{
    long bhead_off = bend_off - XMODEM_BLOCK_SIZE;
    long phead_off = phdr->offset;
    long pend_off = phead_off + phdr->file_size;
    long a = bhead_off > phead_off ? bhead_off : phead_off;
    long b = bend_off < pend_off ? bend_off : pend_off;
    if (a < b) {
        memcpy((char *)phdr->physical_addr + (a - phead_off), block + (a - bhead_off), b-a);
    }
}

char *elf_boot(char *blockbuf, char *headerbuf)
{
    long offset = 0, end_header;
    int i, r;
    struct elf_header *ehdr = (struct elf_header *) headerbuf;
    unsigned char block_number = 1;
    struct elf_program_header *phdr;
    char *curblock;

    r = xmodem_recv_block(headerbuf, block_number++, 0);
    if (r <= 0) return NULL;
    offset += XMODEM_BLOCK_SIZE;
    if (elf_check(ehdr) < 0) {
        xmodem_can();
        return NULL;
    }

    end_header = ehdr->program_header_offset + ehdr->program_header_size * ehdr->program_header_num;
    while (offset < end_header) {
        r = xmodem_recv_block(headerbuf + offset, block_number++, 1);
        if (r <= 0) return NULL;
        offset += XMODEM_BLOCK_SIZE;
    }
    curblock = headerbuf + offset - XMODEM_BLOCK_SIZE;

    phdr = (struct elf_program_header *) (headerbuf + ehdr->program_header_offset);
    for (i = 0; i < ehdr->program_header_num; i++) {
        long e;

        if (phdr->type != 1)
            continue;

        e = phdr->offset + phdr->file_size;
        while (1) {
            load_block(curblock, offset, phdr);
            if (e <= offset) break;

            r = xmodem_recv_block(blockbuf, block_number++, 1);
            if (r <= 0) return NULL;
            offset += XMODEM_BLOCK_SIZE;
            curblock = blockbuf;
        }
        
        phdr = (struct elf_program_header *) ((char *)phdr + ehdr->program_header_size); // phdr++でも良さそうだが一応
    }

    while (xmodem_recv_block(blockbuf, block_number++, 1) > 0)  /* 読み終える */
        ;

    // bssクリア
    phdr = (struct elf_program_header *) (headerbuf + ehdr->program_header_offset);
    for (i = 0; i < ehdr->program_header_num; i++, phdr++) {
        memset((char *)phdr->physical_addr + phdr->file_size, 0, phdr->memory_size - phdr->file_size);
    }

    return (char *)ehdr->entry_point;
}
